﻿using System;
using System.IO;
using System.Xml;
using Ionic.Zip;

namespace EpubSqueaker
{
	public static class NookTools
	{
		/// <summary>
		/// Rewrite the &lt;meta name="cover" /&gt; tag in an ePub OPF file to conform to
		/// the attribute order a Nook expects (name then value).
		/// </summary>
		/// <param name="fileName">Full path to the EPUB file</param>
		public static bool UpdateCoverMetadata(string fileName)
		{
			bool returnVal = false;

			using (ZipFile zip = ZipFile.Read(fileName))
			{
				returnVal = UpdateCoverMetadata(zip);
			}
			
			return returnVal;
		}

		/// <summary>
		/// Rewrite the &lt;meta name="cover" /&gt; tag in an ePub OPF file to conform to
		/// the attribute order a Nook expects (name then value).
		/// </summary>
		/// <param name="epub">Reference to the EPUB file</param>
		public static bool UpdateCoverMetadata(ZipFile epub)
		{
			if (epub == null)
			{
				throw new ArgumentNullException("epub");
			}

			// TODO: This fails utterly if the OPF is XML 1.1 and has a byte-order mark, or is a multibyte encoding.

			bool preserveWhitespace = false; // Parameter here rather than hard-coding down below.
			bool isAnyUpdates = false;

			// Theoretically at this point we know where the OPF file is. Let's open 'er up!
			foreach (ZipEntry zeOpf in EpubTools.OpfFilesInEpub(epub))
			{
				XmlDocument opf = new XmlDocument();
				using (var streamOpf = new MemoryStream())
				{
					zeOpf.Extract(streamOpf);
					streamOpf.Seek(0, SeekOrigin.Begin);

					// HACK: Convert from XML 1.1 to XML 1.0 if necessary, since Microsoft's XML tools only support 1.0.
					if (XmlTools.ConvertXml11ToXml10(streamOpf))
					{
						System.Diagnostics.Debug.WriteLine("Updated OPF " + epub.Name);
					}

					opf.PreserveWhitespace = preserveWhitespace;
					opf.Load(streamOpf);
				}

				bool foundCover = false;

				// Set up our XML namespace information.
				XmlNamespaceManager nsmgr = new XmlNamespaceManager(opf.NameTable);
				nsmgr.AddNamespace("opf", "http://www.idpf.org/2007/opf");

				// Find the meta tags, if any.
				foreach (XmlNode metaCover in opf.SelectNodes("//opf:package/opf:metadata/opf:meta[@name=\"cover\"]", nsmgr))
				{
					XmlAttribute coverNameNode = metaCover.Attributes["name"];
					foundCover = true;
					bool updatedAttributes = false;
					XmlAttribute insertAfterAttribute = coverNameNode;

					while (metaCover.Attributes[0] != coverNameNode)
					{
						XmlAttribute coverOtherAttribute = metaCover.Attributes[0];
						metaCover.Attributes.Remove(coverOtherAttribute);
						metaCover.Attributes.InsertAfter(coverOtherAttribute, insertAfterAttribute);
						insertAfterAttribute = coverOtherAttribute;

						updatedAttributes = true;
					}

					if (updatedAttributes)
					{
						// Save the XML to a stream, and prep the stream for reading.
						using (var opfStream = new MemoryStream())
						{
							opf.Save(opfStream);
							opfStream.Seek(0, SeekOrigin.Begin);

							// Write the stream to the ZIP file.
							epub.UpdateEntry(zeOpf.FileName, opfStream);
							epub.Save();
							isAnyUpdates = true;
						}
					}
				}

				if (!foundCover)
				{
					// Didn't find the cover, need to look in the OPF for a Mobipocket cover image.
					foreach (XmlNode reference in opf.SelectNodes("//opf:package/opf:guide/opf:reference[@type=\"other.ms-coverimage-standard\"]", nsmgr))
					{
						// Okay, we know the HREF for our cover. Now we need to find the matching ID.
						foreach (XmlNode item in opf.SelectNodes("//opf:package/opf:manifest/opf:item", nsmgr))
						{
							// Save our split to a separate variable first so we can access
							// the length (to find the last element) easily.
							var pathElements = item.Attributes["href"].Value.Split('\\');
							if ((pathElements.Length > 0) && (pathElements[pathElements.Length - 1].Equals(reference.Attributes["href"].Value, StringComparison.OrdinalIgnoreCase)))
							{
								System.Diagnostics.Debug.WriteLine("Found cover via <guide>: " + epub.Name);

								XmlNode metadata = opf.SelectSingleNode("//opf:package/opf:metadata", nsmgr);
								XmlElement newCover = opf.CreateElement("meta", metadata.NamespaceURI);
								newCover.SetAttribute("name", "cover");
								newCover.SetAttribute("content", item.Attributes["id"].Value);
								metadata.AppendChild(newCover);

								// Save the XML to a stream, and prep the stream for reading.
								using (var opfStream = new MemoryStream())
								{
									opf.Save(opfStream);
									opfStream.Seek(0, SeekOrigin.Begin);

									// Write the stream to the ZIP file.
									epub.UpdateEntry(zeOpf.FileName, opfStream);
									epub.Save();
									isAnyUpdates = true;
								}

								foundCover = true;
								break;
							}
						}

						if (foundCover)
						{
							break;
						}
					}
				}
			}

			return isAnyUpdates;
		}

		/// <summary>
		/// Check for Calibre bookmarks.
		/// </summary>
		/// <param name="fileName"></param>
		public static bool RemoveCalibreBookmarks(string fileName)
		{
			var toReturn = false;

			using (ZipFile zip = ZipFile.Read(fileName))
			{
				toReturn = RemoveCalibreBookmarks(zip);
			}

			return toReturn;
		}

		/// <summary>
		/// Check for Calibre bookmarks.
		/// </summary>
		/// <param name="fileName"></param>
		public static bool RemoveCalibreBookmarks(ZipFile epub)
		{
			if (epub == null)
			{
				throw new ArgumentNullException("epub");
			}

			var toReturn = false;

			foreach (ZipEntry zeContainer in epub.SelectEntries("calibre_bookmarks.txt", "META-INF"))
			{
				epub.RemoveEntry(zeContainer);
				epub.Save();
				toReturn = true;
			}

			return toReturn;
		}
	}
}
